#include <iostream>
#include <sstream>

#include "OpenCLTestingProcessor.h"
#include "ChannelsSendingTester.h"
#include "ChannelsSendingTester2.h"

#include "Timer.h"
#include "Experiment.h"

#include "Tester.h"


static const int MB_in_bytes = 1048576;
void printArray(double* arr, int size);
void printTime(double time);



#pragma mark validate

void validate() {
    
    OpenCLTestingProcessor openCLProc;
    
    {
        openCLProc.setBuffers(128);
        float* data = new float[128];
        
        openCLProc.transferDataToGPU();
        openCLProc.executeTheSimplestKernel();
        openCLProc.transferDataFromGPU(data);
        
        openCLProc.setDataValue(5);
        
        openCLProc.transferDataToGPU();
        
        openCLProc.setDataValue(7);
        
        openCLProc.transferDataToGPU();
        openCLProc.executeTheSimplestKernel();
        openCLProc.transferDataFromGPU(data);
    }
    
    {
        float* data = new float[1000];
        simplyKernelTest(1000);
        openCLProc.transferDataFromGPU(data);
        data[1] = 58;
    }
    
}



#pragma mark >>test<<

void simplyKernelTest( unsigned int sizeOfDataTransfered, unsigned int repetition)
{
    using namespace std;
    OpenCLTestingProcessor openCLProc;
    Timer timer;
    
    openCLProc.setBuffers(sizeOfDataTransfered);
    float* data = new float[sizeOfDataTransfered];
    
    Timer mainTimer;
    double theWholeTime = 0;
    double *times = new double [repetition];
    
    mainTimer.start();
    for(int i = 0; i < repetition; ++i) {
        timer.start();
        openCLProc.transferDataToGPU();
        openCLProc.executeTheSimplestKernel();
        openCLProc.transferDataFromGPU(data);
        openCLProc.executeCommands();
        timer.stop();
        times[i] = timer.getTime() * 1000; //ms
        theWholeTime += timer.getTime();
    }
    mainTimer.stop();
    
    for(int i = 0; i < sizeOfDataTransfered; ++i)
        if( data[i] != 1)
            cout << "ERROR!!!!!!!!!!!!!!!!!" << endl;
    
    double averageTime = mainTimer.getTime() / repetition;
    double maximumDSP = (double)sizeOfDataTransfered/(double)44100;
    
    cout << "***" << endl;
    cout << "Executing simply kernel." << endl;
    cout << "***" << endl;
    cout << "Data size: " << sizeOfDataTransfered << endl;
    cout << "Repetition: " << repetition << endl;
    cout << "Average time: \t\t\t\t\t\t"; printTime(averageTime); cout << endl;
    cout << "Maximum DSP (one channel, 44100Hz): "; printTime(maximumDSP); cout << endl;
    cout << "1/2 of maximum DSP (one channel, 44100Hz): "; printTime(maximumDSP/2); cout << endl;
    cout << "***" << endl;
    cout << "Main timer: "; printTime(mainTimer.getTime()); cout << endl;
    cout << "Sum of times: "; printTime(theWholeTime); cout << endl;
    cout << "Times results from each [ms]:" << endl;
    printArray(times, repetition);
    cout << "***" << endl;
    cout << endl << endl;
    
    delete [] data;
    delete [] times;    
}

void latencyBandwidthTest( unsigned int sizeOfDataTransfered, unsigned int repetition)
{
    using namespace std;
    OpenCLTestingProcessor openCLProc;
    Timer timer;

    
    //Latency
    openCLProc.setBuffers(1);  //< 1float transfering
    timer.start();
    for(int i=0; i < repetition; ++i)
        openCLProc.transferDataToGPU();
    timer.stop();
    //
    double averageLatency = timer.getTime() / repetition;
    //
    cout << "Latency: "; printTime(averageLatency); cout << endl;
    
    
    
    //Speed
    openCLProc.setBuffers( sizeOfDataTransfered );
    timer.start();
    for(int j=0; j < repetition; ++j)
        openCLProc.transferDataToGPU();
    timer.stop();
    //
    double bandwidth = sizeOfDataTransfered/(timer.getTime()/repetition-averageLatency)/MB_in_bytes;
    //
    cout << "Bandwidth (size of data transfered: "<< sizeOfDataTransfered << "): "<< bandwidth << " MB/s" << endl;
    cout << "Time per one transfer: "; printTime(timer.getTime()/repetition); cout << endl << endl;
    
}

TrialsSet<double> bufferForEachChannel_testFunction (const unsigned int numOfChannels, const unsigned int lengthOfBlock, const unsigned int numOfBlocks)
{
    using namespace std;
    
    ChannelsSendingTester tester;
    Timers timer(numOfBlocks);
    double timeOfCallCommandsFun = 0;
    double timeOfFlushAndFinish = 0;
    
    
    //Allocation
    float*** blocksIn, ***blocksOut;
    blocksIn = new float**[numOfBlocks];
    blocksOut = new float**[numOfBlocks];
    
    for(int i = 0; i < numOfBlocks; ++i) {
        blocksIn[i] = new float*[numOfChannels];
        blocksOut[i] = new float*[numOfChannels];
    }
    
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j) {
            blocksIn[i][j] = new float [lengthOfBlock];
            blocksOut[i][j] = new float [lengthOfBlock];
        }
    //.
    
    //Init source
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j)
            for(int l = 0; l < lengthOfBlock; ++l)
                blocksIn[i][j][l] = l + i*lengthOfBlock;
    //.
    
    //Create buffers
    tester.manyChannels(blocksIn[0], numOfChannels, lengthOfBlock, blocksOut[0], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
    timeOfCallCommandsFun = 0;
    timeOfFlushAndFinish = 0;
    //.
    
    //TEST
    timer.startMain();
    for(int i = 0; i < numOfBlocks; ++i) {
        timer.startTrial();
        tester.manyChannels(blocksIn[i], numOfChannels, lengthOfBlock, blocksOut[i], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
        timer.stopTrial();
    }
    timer.stopMain();
    //.
    
    //Check
    bool successCopy = true;
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j)
            for(int l = 0; l < lengthOfBlock; ++l)
                if( blocksIn[i][j][l] != blocksOut[i][j][l] ) {
                    successCopy = false;
                }
    
    
    if(!successCopy)
        throw NotExpectedProcessingResult_Exception();
    //.
    
    double averageTime = timer.getMainTime() / numOfBlocks;
    double maximumDSP = (double)lengthOfBlock/(double)44100;
    
    //Description
    stringstream stringStream;
    stringStream << "***" << endl;
    stringStream << "ManyChannels. Buffer per channel. One channel kernel." << endl;
    stringStream << "***" << endl;
    stringStream << "Time of calling commands functions (sum): " << timeOfCallCommandsFun*1000 << "ms" << endl;
    stringStream << "Time of flushing and finishing (sum): " << timeOfFlushAndFinish * 1000 << "ms" << endl;
    stringStream << "Proportion: " << timeOfCallCommandsFun/timeOfFlushAndFinish << " : 1" << endl;
    stringStream << "***" << endl;
    stringStream << "Number of channels: " << numOfChannels << endl;
    stringStream << "Number of blocks: " << numOfBlocks << endl;
    stringStream << "Block size: " << lengthOfBlock << endl;
    stringStream << "Average time: \t\t\t\t\t\t"; printTime(averageTime); stringStream << endl;
    stringStream << "Maximum DSP (44100Hz): "; printTime(maximumDSP); stringStream << endl;
    stringStream << "***" << endl;
    stringStream << endl << endl;
    cout << stringStream.str();
    //.
    
    //Prepare result TrialsSet<double>
    double* trials;
    unsigned int trialsNumber;
    std::string description(stringStream.str());
    timer.getTrialsTimes((const double**)&trials, &trialsNumber);
    TrialsSet<double> trialsSet(trials, trialsNumber, description );
    
    return trialsSet;
    //.
}

TrialsSet<double> oneBufferForAllChannels_testFunction (const unsigned int numOfChannels, const unsigned int lengthOfBlock, const unsigned int numOfBlocks)
{
    using namespace std;
    
    //Declarations
    ChannelsSendingTester2 tester;
    Timers timer(numOfBlocks);
    
    double timeOfCallCommandsFun = 0;
    double timeOfFlushAndFinish = 0;
    
    float** blocksIn, **blocksOut;
    blocksIn = new float*[numOfBlocks];
    blocksOut = new float*[numOfBlocks];
    //.
    
    //Allocation for in, out
    for(int i = 0; i < numOfBlocks; ++i) {
        blocksIn[i] = new float [lengthOfBlock * numOfChannels];
        blocksOut[i] = new float [lengthOfBlock * numOfChannels];
    }
    //.
    
    //Init source
    for(int i = 0; i < numOfBlocks; ++i)
        for(int l = 0; l < (lengthOfBlock * numOfChannels); ++l)
            blocksIn[i][l] = l;
    //.
    
    //Create gpu buffers
    tester.manyChannels(blocksIn[0], numOfChannels, lengthOfBlock, blocksOut[0], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
    timeOfCallCommandsFun = 0;
    timeOfFlushAndFinish = 0;
    //.
    
    //MEASUREMENT
    timer.startMain();
    for(int i = 0; i < numOfBlocks; ++i) {
        timer.startTrial();
        tester.manyChannels(blocksIn[i], numOfChannels, lengthOfBlock, blocksOut[i], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
        timer.stopTrial();
    }
    timer.stopMain();
    //.
    
    //Check
    bool successCopy = true;
    for(int i = 0; i < numOfBlocks; ++i)
        for(int l = 0; l < (lengthOfBlock * numOfChannels); ++l)
            if( blocksIn[i][l] != blocksOut[i][l] ) {
                successCopy = false;
            }
    if(!successCopy)
        cout << "In != Out !!!!!!!!!!" << endl;
    //.
    
    double averageTime = timer.getMainTime() / numOfBlocks;
    double maximumDSP = (double)lengthOfBlock/(double)44100;
    
    //Printing
    cout << "***" << endl;
    cout << "ManyChannels2. Multichannel kernel." << endl;
    cout << "***" << endl;
    cout << "Time of calling commands functions (sum): " << timeOfCallCommandsFun*1000 << "ms" << endl;
    cout << "Time of flushing and finishing (sum): " << timeOfFlushAndFinish * 1000 << "ms" << endl;
    cout << "Proportion: " << timeOfCallCommandsFun/timeOfFlushAndFinish << " : 1" << endl;
    cout << "***" << endl;
    cout << "Number of channels: " << numOfChannels << endl;
    cout << "Number of blocks: " << numOfBlocks << endl;
    cout << "Block size: " << lengthOfBlock << endl;
    cout << "Average time: \t\t\t\t\t\t"; printTime(averageTime); cout << endl;
    cout << "Maximum DSP (44100Hz): "; printTime(maximumDSP); cout << endl;
    cout << "***" << endl;
    cout << endl << endl;
    //.
}



#pragma mark helper function

void printArray(double* arr, int size) {
    
    for(int i=0; i<size; ++i) {
        std::cout<<"["<<i<<"] "<<arr[i]<<std::endl;
    }
    std::cout<<std::endl;
    
}

void printTime(double time) {
    std::cout << time << "s\t" << time * 1000 /*thousand*/ << "ms\t" << time*1000000 /*billion*/ << "micro_s";
}



